/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package org.xeerkat.agent.tools;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.infoset.xml.Document;
import org.infoset.xml.Element;
import org.infoset.xml.Name;
import org.restlet.Client;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.ChallengeResponse;
import org.restlet.data.ChallengeScheme;
import org.restlet.data.Method;
import org.restlet.data.Reference;
import org.restlet.routing.Template;
import org.restlet.util.Resolver;
import org.xeerkat.agent.services.Service;
import org.xeerkat.agent.services.ServiceRegistry;
import org.xeerkat.agent.tools.AgentComponent.ManagementListener;
import org.xeerkat.xmpp.ID;

/**
 *
 * @author alex
 */
public class AgentManager implements ManagementListener {

   public static URI NAMESPACE = URI.create("http://www.xeerkat.org/Vocabulary/Agent/2008/1/0");
   public static Name ADD_SERVICE = Name.create(NAMESPACE,"add-service");
   public static Name REPLACE_SERVICE = Name.create(NAMESPACE,"replace-service");
   public static Name REMOVE_SERVICE = Name.create(NAMESPACE,"remove-service");
   public static Name INSTALL = Name.create(NAMESPACE,"install");
   public static Name COPY = Name.create(NAMESPACE,"copy");
   public static Name DELETE = Name.create(NAMESPACE,"delete");
   AgentComponent agentComponent;
   Set<ID> allowFrom;
   Set<String> allowDomains;
   Resolver<String> environment;
   public AgentManager(AgentComponent agentComponent,Resolver<String> environment) {
      this.agentComponent = agentComponent;
      this.allowFrom = agentComponent.allowFrom;
      this.allowDomains = agentComponent.allowDomains;
      this.environment = environment;
   }
   
   public Logger getLogger() {
      return agentComponent.getLogger();
   }
   
   public Set<ID> getAllowFrom() {
      return allowFrom;
   }
   
   public void setAllowFrom(Set<ID> idSet)
   {
      this.allowFrom = idSet;
   }
   
   public Set<String> getAllowDomains() {
      return allowDomains;
   }
   
   public void setAllowDomains(Set<String> nameSet)
   {
      this.allowDomains = nameSet;
   }
   
   public void onManagement(ID to,ID from,String method,String username,String password,Document message)
   {
      agentComponent.getLogger().info("Received management message from "+from);
      if (!allowFrom.contains(new ID(from.toXMPPId()))) {
         agentComponent.getLogger().warning("Unauthorized management message from "+from);
         return;
      }
      if (!authorize(to,from,method,username,password)) {
         agentComponent.getLogger().warning("Authorization failed for management from "+from+", username="+username);
         return;
      }
      agentComponent.getLogger().info("Management authorized.");
      handle(to,from,message);
    }
   
   protected boolean authorize(ID to,ID from,String method,String username,String password)
   {
      return false;
   }
   
   protected boolean allowDownload(URI archive)
   {
      String host = archive.getHost();
      return archive.getScheme().equals("file") || host!=null && allowDomains.contains(host);
   }
   
   protected void handle(ID to,ID from,Document message)
   {
      Element top = message.getDocumentElement();
      Name name = top.getName();
      boolean changed = false;
      if (name.equals(INSTALL)) {
         String uri = top.getAttributeValue("archive");
         try {
            install(to,from,new URI(uri),top.getAttributeValue("method"),top.getAttributeValue("username"),top.getAttributeValue("password"));
            changed = true;
         } catch (URISyntaxException ex) {
            agentComponent.getLogger().severe("Cannot create archive URI from "+uri+" for install due to: "+ex.getMessage());
         }
      } else if (name.equals(ADD_SERVICE)) {
         Iterator<Element> services = top.getElementsByName(ServiceRegistry.SERVICE);
         while (services.hasNext()) {
            addService(to,from,services.next());
            changed = true;
         }
      } else if (name.equals(REPLACE_SERVICE)) {
         Iterator<Element> services = top.getElementsByName(ServiceRegistry.SERVICE);
         while (services.hasNext()) {
            replaceService(to,from,services.next());
            changed = true;
         }
      } else if (name.equals(REMOVE_SERVICE)) {
         String path = top.getAttributeValue("path");
         String qname = top.getAttributeValue("name");
         String iface = top.getAttributeValue("interface");
         try {
            Name serviceName = top.getNamespaceScope().expandName(qname, false);
            removeService(to,from,serviceName,path,new URI(iface));
            changed = true;
         } catch (Exception ex) {
            agentComponent.getLogger().severe("Cannot remove service "+qname+", interface "+iface+" due to: "+ex.getMessage());
         }
      } else {
         agentComponent.getLogger().severe("Unrecognized management message: "+name);
      }
      // if changed
      if (changed) {
         try {
            agentComponent.updateHosts();
            agentComponent.getConfiguration().save();
         } catch (Exception ex) {
            agentComponent.getLogger().log(Level.SEVERE,"Cannot save agent configuration.",ex);
         }
      }
      
   }
   public AgentComponent.AgentEnvironment getEnvironment(ID to)
   {
      AgentComponent.AgentEnvironment agentEnv = agentComponent.getAgentEnvironment(to);
      if (agentEnv==null) {
         agentEnv = agentComponent.getAgentEnvironment(new ID(to.toXMPPId()));
      }
      return agentEnv;
   }
   
   public Configuration.Agent getAgentConfiguration(ID to)
   {
      Configuration.Agent agent = agentComponent.getConfiguration().getAgents().get(to);
      if (agent==null) {
         agent = agentComponent.getConfiguration().getAgents().get(new ID(to.toXMPPId()));
      }
      return agent;
   }
   
   protected String expand(String target)
   {
      Template template = new Template(target);
      return template.format(environment);
   }
   
   protected void expandAttribute(Element owner,String name)
   {
      String target = owner.getAttributeValue(name);
      if (target!=null) {
         Template template = new Template(target);
         owner.setAttributeValue(name,template.format(environment));
      }
   }
   
   protected void expandAttribute(Element owner,Name name)
   {
      String target = owner.getAttributeValue(name);
      if (target!=null) {
         Template template = new Template(target);
         String expanded = template.format(environment);
         //getLogger().info(name+": "+target+" -> "+expanded);
         owner.setAttributeValue(name,expanded);
      }
   }
   
   protected void expandElement(Element target)
   {
      for (Name name : target.getAttributes().keySet()) {
         expandAttribute(target,name);
      }
      // TODO: make this non-recursive
      Iterator<Element> children = target.getElementChildren();
      while (children.hasNext()) {
         expandElement(children.next());
      }
   }
   
   public boolean replaceService(ID to,ID form,Element serviceDef)
   {
      try {
         agentComponent.getLogger().info("Replacing service on agent "+to);
         AgentComponent.AgentEnvironment agentEnv = getEnvironment(to);
         if (agentEnv==null) {
            agentComponent.getLogger().log(Level.SEVERE,"Cannot find environment for agent id "+to);
            return false;
         }
         Configuration.Agent agent = getAgentConfiguration(to);
         if (agent==null) {
            agentComponent.getLogger().log(Level.SEVERE,"Cannot find agent "+to);
            return false;
         }
         expandElement(serviceDef);
         Service newService = ServiceRegistry.createService(serviceDef);
         Service oldService = agent.getLocalServices().get(newService.getName());
         if (oldService!=null) {
            agentEnv.unloadService(oldService.getPath(),oldService.getInterface());
         } else {
            agentComponent.getLogger().warning("Cannot find service named "+newService.getName()+" to replace, continuing to add new service...");
         }
         
         agent.getLocalServices().put(newService.getName(), newService);
         newService.init();
         agentEnv.loadService(newService);
         agentComponent.getLogger().info("Service replaced for agent "+to);
         return true;
      } catch (Exception ex) {
         agentComponent.getLogger().log(Level.SEVERE,"Cannot add service due to exception.",ex);
         return false;
      }
   }
   
   public boolean addService(ID to,ID from,Element serviceDef)
   {
      try {
         agentComponent.getLogger().info("Adding service to agent "+to);
         expandElement(serviceDef);
         Service service = ServiceRegistry.createService(serviceDef);
         Configuration.Agent agent = getAgentConfiguration(to);
         if (agent==null) {
            agentComponent.getLogger().log(Level.SEVERE,"Cannot find agent "+to);
            return false;
         }
         agent.getLocalServices().put(service.getName(), service);
         service.init();
         AgentComponent.AgentEnvironment agentEnv = getEnvironment(to);
         if (agentEnv==null) {
            agentComponent.getLogger().log(Level.SEVERE,"Cannot find environment for agent id "+to);
            return false;
         }
         agentEnv.loadService(service);
         agentComponent.getLogger().info("Service added for agent "+to);
         return true;
      } catch (Exception ex) {
         agentComponent.getLogger().log(Level.SEVERE,"Cannot add service due to exception.",ex);
         return false;
      }
   }
   
   public boolean removeService(ID to,ID from,Name name,String path,URI iface)
   {
      try {
         AgentComponent.AgentEnvironment agentEnv = getEnvironment(to);
         if (agentEnv==null) {
            agentComponent.getLogger().log(Level.SEVERE,"Cannot find environment for agent id "+to);
            return false;
         }
         Configuration.Agent agent = getAgentConfiguration(to);
         if (agent==null) {
            agentComponent.getLogger().log(Level.SEVERE,"Cannot find agent "+to);
            return false;
         }
         agentEnv.unloadService(path,iface);
         Service service = agent.getLocalServices().remove(name);
         if (service==null) {
            agentComponent.getLogger().warning("Cannot find service named "+name+" to remove.");
         } else if (iface!=null && !service.getInterface().equals(iface)) {
            agentComponent.getLogger().warning("Removed service interface "+service.getInterface()+" does not match "+iface);
         }
         agentComponent.getLogger().info("Service removed for agent "+to);
         return true;
      } catch (Exception ex) {
         agentComponent.getLogger().log(Level.SEVERE,"Cannot remove service at path "+path+", interface "+iface+" due to exception.",ex);
         return false;
      }
   }
   
   public void install(ID to,ID from,URI archive,String method,String username, String password)
   {
      if (!allowDownload(archive)) {
         getLogger().severe("Unauthorized domain "+archive.getHost()+" for upgrade archive "+archive);
         return;
      }
      agentComponent.getLogger().info("Installing from archive "+archive);
      Reference ref = new Reference(archive.toString());
      Client client = agentComponent.getContext().getClientDispatcher();
      Request request = new Request(Method.GET,ref);
      if (username!=null) {
         request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.valueOf(method),username,password));
      }
      Response response = client.handle(request);
      if (!response.getStatus().isSuccess()) {
         agentComponent.getLogger().severe("Cannot download "+archive+" due to status "+response.getStatus().getCode());
         return;
      }
      
      File archiveFile;
      try {
         File tmpDir = File.createTempFile("update", "/");
         tmpDir.delete();
         tmpDir.mkdirs();
         archiveFile = new File(tmpDir,ref.getLastSegment());
         FileOutputStream os = new FileOutputStream(archiveFile);
         InputStream is = response.getEntity().getStream();
         byte [] buffer = new byte[16384];
         int len;
         while ((len=is.read(buffer))>=0) {
            os.write(buffer,0,len);
         }
         is.close();
         os.close();
         
      } catch (IOException ex) {
         agentComponent.getLogger().severe("I/O error while downloading archive: "+ex.getMessage());
         return;
      }
      if (verifyArchive(archiveFile)) {
         try {
            Runnable installer = new InstallProcess(to,from,this,archiveFile.toURL(),environment);
            installer.run();
         } catch (Exception ex) {
            agentComponent.getLogger().log(Level.SEVERE,"Cannot run install process due to exception.",ex);
         }
      }
   }
   
   protected boolean verifyArchive(File archiveFile)
   {
      return true;
   }
   
}
